using System;
using System.Collections.Generic;
using System.Text;

using Reflector.CodeModel;
using XMIAddin.Xmi;

namespace XMIAddin.ReflectorLoad
{
    /// <summary>
    /// The ModelLoader is responsible for creating a Uml/Xmi model of an
    /// assembly selected in Reflector. The reflector classes are used to
    /// inspect the assembly and construct appropriate model elements. The 
    /// details can be retrieved from the Model property once the load is
    /// complete.
    /// </summary>
    public class ModelLoader
    {
        private readonly ElementCache _Cache;
        private readonly UmlModel _Model;

        /// <summary>
        /// The Cache that the Loader uses to store ModelElements it has loaded.
        /// </summary>
        /// <remarks>
        /// The cache stores all of the elements loaded to allow
        /// references to be setup. When an element is created it
        /// is added to the Cache. When a reference is required the 
        /// type is loaded either directly or from this cache.
        /// </remarks>
        internal ElementCache Cache
        {
            get { return _Cache; }
        }

        /// <summary>
        /// This is the model object. The loader will populate this with 
        /// elements from the <c>XMIAddin.Xmi</c> namespace.
        /// </summary>
        public UmlModel Model
        {
            get { return _Model; }
        } 


        public ModelLoader() : this(new UmlModel())
        {}
        
        public ModelLoader(UmlModel target)
        {
            _Model = target;
            _Cache = new ElementCache(Model, this);

            //Initialise with elements I will always want
            _Model.Add(UmlStereotype.REALIZE);
        }

        public void Load(IAssembly toLoad)
        {
            Cache.FullyLoad(toLoad);
            UmlOwner owner = Model;

            foreach (IModule module in toLoad.Modules)
            {
                foreach (ITypeDeclaration type in module.Types)
                {
                    //Fetch the containing package...
                    UmlOwner package = LoadPackage(type.Namespace);
                    if (package == null) owner = Model;
                    else owner = package;

                    Load(type);
                }
            }
        }

        /// <summary>
        /// Loads a .NET namespace as a UML Package.
        /// </summary>
        /// <param name="fullname"></param>
        /// <returns></returns>
        public UmlOwner LoadPackage(string fullname)
        {
            UmlPackage package = Cache.Locate<UmlPackage>(fullname, false);
            if (package != null) return package;

            UmlOwner owner = Model;
            string[] parts = fullname.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
            string currentPackage = "";            

            foreach (string ending in parts)
            {                
                currentPackage += ending; //build the full name of each package

                if (currentPackage.Length > 0) //if this requires a package
                {
                    //try to see if we already have this package
                    package = Cache.Locate<UmlPackage>(currentPackage, false);
                    if (package == null) //no package there...
                    {
                        package = new UmlPackage();
                        package.Name = ending;
                        owner.Add(package); //add to package structure in UML
                        owner = package;
                        Cache.Elements.Add(currentPackage, package);
                    }
                    else
                    {
                        owner = package;
                    }

                    currentPackage += ".";
                }                
            }

            if (currentPackage.Length == 0) return Model;
            return package;
        }

        /// <summary>
        /// Load the details for the declared type. This will search the ElementCache for the container in which this
        /// type belongs.
        /// </summary>
        /// <param name="type">the type to be loaded</param>
        internal UmlClassifier Load(ITypeDeclaration type)
        {
            //ignore <Module>
            if (type.Name == "<Module>") return null;

            UmlOwner owner;
            if (type.Owner is ITypeDeclaration)
            {
                owner = Load(type.Owner as ITypeDeclaration) as UmlClassifier;
            }
            else
            {
                owner = LoadPackage(type.Namespace);
            }

            return Load(type, owner);
        }

        private UmlClassifier Load(ITypeDeclaration type, UmlOwner owner)
        {
            UmlClassifier data;
            //Check if it is already there
            data = Cache.Locate<UmlClassifier>(type, false);
            if (data != null) return data;
   
            //Get a new UmlClassifier
            data = CreateClassifierFor(type);

            //Load common features
            data.Name = type.Name;
            if (type.GenericArguments != null)
            {
                for (int i = 0; i < type.GenericArguments.Count; i++)
                {
                    if (i == 0) data.Name += "<";
                    data.Name += type.GenericArguments[i];
                    if (i < type.GenericArguments.Count - 1) data.Name += ", ";
                    if (i == type.GenericArguments.Count - 1) data.Name += ">";
                }
            }
            data.IsAbstract = type.Abstract;

            //NOTE: Add to cache as early as possible to correct circular references
            Cache.Elements.Add(type, data);

            LoadVisibility(type.Visibility, data);
            LoadClassifierFeatures(type, data);
            LoadInheritance(type, data);


            //Load into model and Cache
            owner.Add(data);

            return data;
        }

        private void LoadInheritance(ITypeDeclaration type, UmlClassifier data)
        {
            foreach (ITypeReference ty in type.Interfaces)
            {
                UmlAbstraction abstr = new UmlAbstraction();
                abstr.ModelElementStereotype = UmlTypedElementType.Create(UmlStereotype.REALIZE);
                abstr.DependencyClient = UmlTypedElementType.Create(data);
                abstr.DependencySupplier = UmlTypedElementType.Create(Load(ty.Resolve()));

                Model.Add(abstr);
            }

            if (type.BaseType != null)
            {
                UmlGeneralization gen = new UmlGeneralization();
                gen.GeneralizationChild = UmlTypedElementType.Create(data);
                gen.GeneralizationParent = UmlTypedElementType.Create(Load(type.BaseType.Resolve()));
                Model.Add(gen);
            }
        }

        /// <summary>
        /// Create the correct UmlClassifier for the indicated type. This is a factory
        /// method used to construct UmlClassifiers.
        /// </summary>
        /// <param name="type">The type to create a UmlClassifier for</param>
        /// <returns>A new UmlClassifier object</returns>
        private static UmlClassifier CreateClassifierFor(ITypeDeclaration type)
        {
            UmlClassifier data;
            if (type.Interface)
            {
                data = new UmlInterface();
            }
            //else if(type.
            else if (type.BaseType == null && type.Name == "Object")
            {
                data = new UmlClassifier();
                data.IsRoot = true;
            }
            else if (type.BaseType != null && (type.BaseType.Name == "ValueType" || type.BaseType.Name == "Enum"))
            {
                data = new UmlDataType();
                data.IsLeaf = true;
            }
            else
            {
                data = new UmlClassifier();
            }
            return data;
        }

        private static void LoadVisibility(TypeVisibility visibility, UmlVisibleModelElement data)
        {
            switch (visibility)
            {
                case TypeVisibility.Public: data.Visibility = UmlVisibleModelElement.PUBLIC; break;
                case TypeVisibility.Private: data.Visibility = UmlVisibleModelElement.PRIVATE; break;
                case TypeVisibility.NestedFamily: data.Visibility = UmlVisibleModelElement.PROTECTED; break;
                default: data.Visibility = UmlVisibleModelElement.PACKAGE; break;
            }
        }

        private static void LoadVisibility(MethodVisibility visibility, UmlVisibleModelElement data)
        {
            switch (visibility)
            {
                case MethodVisibility.Public: data.Visibility = UmlVisibleModelElement.PUBLIC; break;
                case MethodVisibility.Private: data.Visibility = UmlVisibleModelElement.PRIVATE; break;
                case MethodVisibility.Family:
                case MethodVisibility.FamilyAndAssembly:
                case MethodVisibility.FamilyOrAssembly: data.Visibility = UmlVisibleModelElement.PROTECTED; break;
                default: data.Visibility = UmlVisibleModelElement.PACKAGE; break;
            }
        }

        private static void LoadVisibility(FieldVisibility visibility, UmlVisibleModelElement data)
        {
            switch (visibility)
            {
                case FieldVisibility.Public: data.Visibility = UmlVisibleModelElement.PUBLIC; break;
                case FieldVisibility.Private: data.Visibility = UmlVisibleModelElement.PRIVATE; break;
                case FieldVisibility.Family:
                case FieldVisibility.FamilyAndAssembly:
                case FieldVisibility.FamilyOrAssembly: data.Visibility = UmlVisibleModelElement.PROTECTED; break;
                default: data.Visibility = UmlVisibleModelElement.PACKAGE; break;
            }
        }

        private void LoadClassifierFeatures(ITypeDeclaration type, UmlClassifier data)
        {
            bool loadAll = Cache.IsFullyLoad(GetAssembly(type));

            foreach (IMethodDeclaration method in type.Methods)
            {
                UmlOperation op = new UmlOperation();
                op.Name = method.Name;
                if(false == loadAll && 
                    (method.Visibility != MethodVisibility.Public && method.Visibility != MethodVisibility.Family))
                {
                    continue; //dont process this method
                }

                LoadVisibility(method.Visibility, data);

                op.IsAbstract = method.Abstract;
                if (method.Static) op.OwnerScope = UmlClassifierFeature.STATIC;

                LoadOperation(op, method, loadAll);

                data.ClassifierFeature.Add(op);
            }

            foreach (IFieldDeclaration field in type.Fields)
            {
                UmlAttribute attr = new UmlAttribute();
                attr.Name = field.Name;
                LoadVisibility(field.Visibility, attr);
                if (field.Static)
                    attr.OwnerScope = UmlClassifierFeature.STATIC;

                if (false == loadAll &&
                    (field.Visibility != FieldVisibility.Public && field.Visibility != FieldVisibility.Family))
                {
                    continue; //dont process this method
                }

                if (loadAll)
                {
                    //Only load types for the current assembly
                    TryLoadType(attr, field.FieldType);
                }

                data.ClassifierFeature.Add(attr);
            }
        }

        private IAssembly GetAssembly(ITypeDeclaration type)
        {
            object owner = type.Owner;
            while (false == owner is IAssembly)
            {
                if (owner is ITypeDeclaration)
                {
                    owner = (owner as ITypeDeclaration).Owner;
                }
                else if (owner is IModule)
                {
                    owner = (owner as IModule).Assembly;
                }
                else
                {
                    throw new ApplicationException("I dont know how to handle this owner type");
                }
            }

            return owner as IAssembly;
        }

        private void LoadOperation(UmlOperation op, IMethodDeclaration method, bool loadAll)
        {
            if (method.ReturnType != null)
            {
                UmlParameter returnValue;
                returnValue = new UmlParameter();
                returnValue.Name = "return";
                returnValue.Kind = UmlParameter.RETURN;

                if(loadAll)
                    TryLoadType(returnValue, method.ReturnType.Type);

                op.Parameters.Add(returnValue);
            }

            foreach (IParameterDeclaration parameter in method.Parameters)
            {
                UmlParameter umlParam = new UmlParameter();
                umlParam.Name = parameter.Name;

                if (parameter.ParameterType is IReferenceType)
                {
                    umlParam.Kind = UmlParameter.INOUT;
                }

                if(loadAll)
                    TryLoadType(umlParam, parameter.ParameterType);

                op.Parameters.Add(umlParam);
            }
        }

        private void TryLoadType(IUmlTyped typedValue, IType iType)
        {
            ITypeReference aRef = iType as ITypeReference;
            if (aRef != null)
            {
                ITypeDeclaration refType;
                refType = aRef.Resolve();

                UmlModelElement element = Cache.Locate<UmlModelElement>(refType, true);
                typedValue.TypedElementType = UmlTypeReference.Create(element);
            }
        }
    }
}
